<!DOCTYPE html>
<!--
Copyright 2016 The Chromium Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file.
-->

<link rel="import" href="/tracing/base/assert_utils.html">
<link rel="import" href="/tracing/core/test_utils.html">
<link rel="import" href="/tracing/extras/chrome/chrome_test_utils.html">
<link rel="import"
    href="/tracing/extras/chrome/chrome_user_friendly_category_driver.html">
<link rel="import" href="/tracing/extras/chrome/estimated_input_latency.html">
<link rel="import" href="/tracing/metrics/system_health/loading_metric.html">
<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html">

<script>
'use strict';

tr.b.unittest.testSuite(function() {
  const getInteractiveTimestamps = tr.e.chrome.getInteractiveTimestamps;
  const getPostInteractiveTaskWindows =
    tr.e.chrome.getPostInteractiveTaskWindows;
  const getNavStartTimestamps = tr.e.chrome.getNavStartTimestamps;
  const expectedQueueingTime = tr.e.chrome.expectedQueueingTime;
  const maxExpectedQueueingTimeInSlidingWindow =
      tr.e.chrome.maxExpectedQueueingTimeInSlidingWindow;
  const weightedExpectedQueueingTime = tr.e.chrome.weightedExpectedQueueingTime;
  const assertRangeEquals = tr.b.assertRangeEquals;

  function newSchedulerTask(startTime, duration) {
    return tr.c.TestUtils.newSliceEx({
      cat: 'toplevel',
      title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
      start: startTime,
      duration
    });
  }

  /**
   * Adds a FrameLoader snapshot to rendererProcess that is used by test FMP
   * candidate slices.
   */
  function addTestFrame(rendererProcess) {
    rendererProcess.objects.addSnapshot(
        'ptr', 'loading', 'FrameLoader', 300, {
          isOutermostMainFrame: true,
          isLoadingMainFrame: true,
          frame: {id_ref: '0xdeadbeef'},
          documentLoaderURL: 'http://example.com'
        });
  }

  function addNavigationStart(mainThread, startNavTime) {
    mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
      cat: 'blink.user_timing',
      title: 'navigationStart',
      start: startNavTime,
      duration: 0.0,
      args: {frame: '0xdeadbeef'}
    }));
  }

  function addNetworkRequest(rendererMain, start, duration) {
    const networkEvents = [];
    rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newSliceEx({
      cat: 'disabled-by-default-network',
      title: 'ResourceLoad',
      start,
      duration,
    }));
  }

  function addFirstContentfulPaint(mainThread, fcpTime) {
    mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
      cat: 'loading',
      title: 'firstContentfulPaint',
      start: fcpTime,
      duration: 0.0,
      args: {frame: '0xdeadbeef'}
    }));
  }

  function addDomContentLoadedEnd(mainThread, dclTime) {
    mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
      cat: 'blink.user_timing',
      title: 'domContentLoadedEventEnd',
      start: dclTime,
      duration: 0.0,
      args: {frame: '0xdeadbeef'}
    }));
  }

  function addSchedulerTask(mainThread, startTime, duration) {
    mainThread.sliceGroup.pushSlice(newSchedulerTask(startTime, duration));
  }

  function addDummyTask(mainThread, startTime) {
    mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
      cat: 'dummy',
      title: 'dummyTitle',
      start: startTime,
      duration: 0.0
    }));
  }

  test('getNavStartTimestamps', () => {
    const model = tr.e.chrome.ChromeTestUtils.newChromeModel(model => {
      const mainThread = model.rendererMain;
      addNavigationStart(mainThread, 0);
      addNavigationStart(mainThread, 10);
      addNavigationStart(mainThread, 30);
    });

    const chromeHelper = model.getOrCreateHelper(
        tr.model.helpers.ChromeModelHelper);
    const rendererHelper = chromeHelper.rendererHelpers[
        model.rendererProcess.pid];
    const navStartTimestamps = getNavStartTimestamps(rendererHelper);

    // It is ok to assert equality for floating point numbers here because
    // the timestamps should remain unmodified.
    assert.deepEqual(navStartTimestamps, [0, 10, 30]);
  });

  /**
   * Checks getInteractiveTimestamps works as intended. If the definition of
   * TTI metric changes, this test may begin to fail and we may need to adjust
   * our EIL implementation.
   */
  test('getInteractiveTimestamps', () => {
    const model = tr.e.chrome.ChromeTestUtils.newChromeModel(model => {
      addTestFrame(model.rendererProcess);

      const mainThread = model.rendererMain;
      addNavigationStart(mainThread, 0);
      addNetworkRequest(mainThread, 0, 50);
      addFirstContentfulPaint(mainThread, 5000);
      addDomContentLoadedEnd(mainThread, 5000);

      addNavigationStart(mainThread, 100000);
      addNetworkRequest(mainThread, 100000, 50);
      addFirstContentfulPaint(mainThread, 110000);
      addDomContentLoadedEnd(mainThread, 110000);

      // To detect when a page has become interactive, we need to find a large
      // enough window of no long tasks. Adding a dummy task sufficiently far
      // away extends the bounds of the model so that it can contain this
      // window. In a non-test scenario, we always record traces for long enough
      // that this is not an issue.
      addDummyTask(mainThread, 900000);
    });

    const interactiveTimestampsMap = getInteractiveTimestamps(model);
    const interactiveTimestamps =
        interactiveTimestampsMap.get(model.rendererProcess.pid);
    assert.deepEqual(
        interactiveTimestamps.sort((a, b) => a - b), [5000, 110000]);
  });

  test('getInteractiveTimestampsMultiRenderer', () => {
    const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
      const rendererProcesses = [];
      // ChromeModel creates one renderer. We create rest of them here.
      for (let i = 1; i <= 5; i++) {
        const rendererProcess = model.getOrCreateProcess(i + 100);
        const mainThread = rendererProcess.getOrCreateThread(i + 100 + 10);
        mainThread.name = 'CrRendererMain';

        addTestFrame(rendererProcess);
        addNavigationStart(mainThread, i * 1000);
        addNetworkRequest(mainThread, i * 1000, 50);
        addFirstContentfulPaint(mainThread, i * 1000 + 50);
        addDomContentLoadedEnd(mainThread, i * 1000 + 2000);
        addNavigationStart(mainThread, i * 10000);
        addNetworkRequest(mainThread, i * 10000, 50);
        addFirstContentfulPaint(mainThread, i * 10000 + 2000);
        addDomContentLoadedEnd(mainThread, i * 10000 + 2000);
        addDummyTask(mainThread, 100000);
      }
    });

    const interactiveTimestampsMap = getInteractiveTimestamps(model);
    assert.deepEqual(interactiveTimestampsMap.get(101), [3000, 12000]);
    assert.deepEqual(interactiveTimestampsMap.get(102), [4000, 22000]);
    assert.deepEqual(interactiveTimestampsMap.get(103), [5000, 32000]);
    assert.deepEqual(interactiveTimestampsMap.get(104), [6000, 42000]);
    assert.deepEqual(interactiveTimestampsMap.get(105), [7000, 52000]);
  });

  test('singlePostInteractiveWindow', () => {
    const interactiveTimestamps = [50];
    const navStartTimestamps = [0];
    const traceEndTimestamp = [100];
    const windows = getPostInteractiveTaskWindows(
        interactiveTimestamps, navStartTimestamps, traceEndTimestamp);
    assert.strictEqual(windows.length, 1);
    assertRangeEquals(windows[0], tr.b.math.Range.fromExplicitRange(50, 100));
  });

  test('multiplePostInteractiveWindows', () => {
    const interactiveTimestamps = [50, 80];
    const navStartTimestamps = [0, 70];
    const traceEndTimestamp = [100];
    const windows = getPostInteractiveTaskWindows(
        interactiveTimestamps, navStartTimestamps, traceEndTimestamp);
    assert.strictEqual(windows.length, 2);
    assertRangeEquals(windows[0], tr.b.math.Range.fromExplicitRange(50, 70));
    assertRangeEquals(windows[1], tr.b.math.Range.fromExplicitRange(80, 100));
  });

  test('postInteractiveWindowWithOneNavigationNeverReachingInteractive', () => {
    const interactiveTimestamps = [50, 90];
    const navStartTimestamps = [0, 60, 70];
    const traceEndTimestamp = [100];
    const windows = getPostInteractiveTaskWindows(
        interactiveTimestamps, navStartTimestamps, traceEndTimestamp);
    assert.strictEqual(windows.length, 2);
    assertRangeEquals(windows[0], tr.b.math.Range.fromExplicitRange(50, 60));
    assertRangeEquals(windows[1], tr.b.math.Range.fromExplicitRange(90, 100));
  });

  test('twoInteractiveTimeStampsWithNoNavStartInBetween', () => {
    const interactiveTimestamps = [50, 75];
    const navStartTimestamps = [0];
    const traceEndTimestamp = [100];
    assert.throws(() => getPostInteractiveTaskWindows(
        interactiveTimestamps, navStartTimestamps, traceEndTimestamp));
  });

  test('expectedQueueingTime_noTasks', () => {
    const window = tr.b.math.Range.fromExplicitRange(0, 1000);
    assert.closeTo(0, expectedQueueingTime(window, []), 1e-6);
  });

  test('expectedQueueingTime_singleTask', () => {
    const window = tr.b.math.Range.fromExplicitRange(0, 1000);
    assert.closeTo(1000 / 2,
        expectedQueueingTime(window, [{start: 0, end: 1000}]),
        1e-6);
  });

  test('expectedQueueingTime_singleTaskStartingBeforeWindow', () => {
    const window = tr.b.math.Range.fromExplicitRange(0, 1000);
    assert.closeTo(1000 / 2,
        expectedQueueingTime(window, [{start: -1, end: 1000}]),
        1e-6);
  });

  test('expectedQueueingTime_singleTaskEndingAfterWindow', () => {
    const window = tr.b.math.Range.fromExplicitRange(0, 1000);
    assert.closeTo(1500,
        expectedQueueingTime(window, [{start: 0, end: 2000}]),
        1e-6);
  });

  test('expectedQueueingTime_singleTaskInsideWindow', () => {
    const window = tr.b.math.Range.fromExplicitRange(0, 1000);
    assert.closeTo(10 / 1000 * 10 / 2,
        expectedQueueingTime(window, [{start: 500, end: 510}]),
        1e-6);
  });

  test('expectedQueueingTime_twoTasksInsideWindow', () => {
    const window = tr.b.math.Range.fromExplicitRange(0, 1000);
    assert.closeTo(10 / 1000 * 10 / 2 + 100 / 1000 * 100 / 2,
        expectedQueueingTime(window,
            [{start: 500, end: 510}, {start: 600, end: 700}]),
        1e-6);
  });

  test('expectedQueueingTime_twoTasksPartiallyInsideWindow', () => {
    const window = tr.b.math.Range.fromExplicitRange(0, 1000);
    assert.closeTo(10 / 1000 * 10 / 2 + 100 / 1000 * (100 + 200) / 2,
        expectedQueueingTime(window,
            [{start: 500, end: 510}, {start: 900, end: 1100}]),
        1e-6);
  });

  test('weightedExpectedQueueingTime', () => {
    const window = tr.b.math.Range.fromExplicitRange(0, 1000);
    assert.closeTo(1000 / 2 * 0.7,
        weightedExpectedQueueingTime(window,
            [{start: 0, end: 1000, weight: 0.7}]),
        1e-6);
  });

  test('maxExpectedQueueingTimeInSlidingWindow_taskOutsideRange', () => {
    assert.closeTo(0,
        maxExpectedQueueingTimeInSlidingWindow(0, 1000, 100,
            [{start: 2000, end: 3000}]),
        1e-6);
  });

  test('maxExpectedQueueingTimeInSlidingWindow_taskInsideRange', () => {
    assert.closeTo(100 / 100 * 100 / 2,
        maxExpectedQueueingTimeInSlidingWindow(0, 1000, 100,
            [{start: 200, end: 300}]),
        1e-6);
  });

  test('maxExpectedQueueingTimeInSlidingWindow_longTask', () => {
    assert.closeTo(100 / 100 * (100 + 200) / 2,
        maxExpectedQueueingTimeInSlidingWindow(0, 1000, 100,
            [{start: 200, end: 400}]),
        1e-6);
  });

  test('maxExpectedQueueingTimeInSlidingWindow_twoTasks', () => {
    assert.closeTo(2 * 10 / 100 * 10 / 2,
        maxExpectedQueueingTimeInSlidingWindow(0, 1000, 100,
            [{start: 200, end: 210}, {start: 290, end: 300}]),
        1e-6);
  });

  test('maxExpectedQueueingTimeInSlidingWindow_taskLargerThanRange', () => {
    assert.closeTo(100 / 100 * (1200 + 1100) / 2,
        maxExpectedQueueingTimeInSlidingWindow(0, 1000, 100,
            [{start: -200, end: 1200}]),
        1e-6);
  });

  test('maxExpectedQueueingTimeInSlidingWindow_multipleTasks', () => {
    assert.closeTo(40 / 100 * 40 / 2,
        maxExpectedQueueingTimeInSlidingWindow(0, 1000, 100, [
          {start: 500, end: 510},
          {start: 510, end: 520},
          {start: 520, end: 530},
          {start: 615, end: 655},
          {start: 1000, end: 2000},
        ]), 1e-6);
  });

  test('maxExpectedQueueingTimeInSlidingWindow_threeTasks', () => {
    assert.closeTo(40 / 100 * 40 / 2 + 20 / 100 * (50 + 30) / 2,
        maxExpectedQueueingTimeInSlidingWindow(0, 1000, 100, [
          {start: 500, end: 510},
          {start: 520, end: 560},
          {start: 600, end: 650},
        ]), 1e-6);
  });

  test('maxExpectedQueueingTimeInSlidingWindow_taskEndsAfterRange', () => {
    assert.closeTo(1 / 100 * (200 + 199) / 2,
        maxExpectedQueueingTimeInSlidingWindow(0, 1000, 100, [
          {start: 500, end: 502},
          {start: 999, end: 1199},
        ]), 1e-6);
  });

  test('maxExpectedQueueingTimeInSlidingWindow_taskStartsBeforeRange', () => {
    assert.closeTo(3 / 100 * 1 / 2 + 20 / 100 * 20 / 2,
        maxExpectedQueueingTimeInSlidingWindow(0, 1000, 100, [
          {start: -10, end: 1},
          {start: 1, end: 2},
          {start: 2, end: 3},
          {start: 80, end: 100},
          {start: 999, end: 1099},
        ]), 1e-6);
  });

  test('maxExpectedQueueingTimeInSlidingWindow_nonFittingWindowThrows', () => {
    assert.throws(() => maxExpectedQueueingTimeInSlidingWindow(0, 10, 100,
        [{start: 0, end: 100}]),
    'The sliding window must fit in the specified time range'
    );
  });

  test('maxExpectedQueueingTimeInSlidingWindow_emptyWindowThrows', () => {
    assert.throws(() => maxExpectedQueueingTimeInSlidingWindow(0, 10, 0,
        [{start: 0, end: 100}]),
    'The window size must be positive number'
    );
  });

  test('maxExpectedQueueingTimeInSlidingWindow_smallOverlapIsTolerated', () => {
    // Allow small floating-point precision error when comparing task
    // end-points for overlapping.
    assert.closeTo((100.01 + 0.01) / 2,
        maxExpectedQueueingTimeInSlidingWindow(0, 1000, 100,
            [{start: 0, end: 100.02}, {start: 100.0, end: 200}]),
        1e-6);
  });
});
</script>
